package com.fzk.core;

import com.fzk.env.conf.DataConf;
import com.fzk.log.Logger;
import com.fzk.utils.MyFileUtil;
import com.fzk.utils.MyHash;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * FileIndex文件：存储文件名-->offset，哈希索引，链表法解决冲突
 * TODO 单个哈希文件写满了，创建新的文件
 *
 * @author fzk
 * @datetime 2023-03-17 16:42
 */
public class FileIndex implements AutoCloseable {
    private static final int Hash_Slot_Size = 4;// 哈希槽大小
    private static final int Index_Size = 20;// 索引条目大小
    /**
     * 非法index，必须将0设为非法index，因为哈希槽数组默认初始值都是0
     */
    private static final int InvalidIndex = 0;
    private final int maxIndexNum;// 索引数组长度
    private final String indexFileName;// 索引文件名
    private final FileChannel fileChannel;// index文件通道
    private final MappedByteBuffer fileMap;// index文件的内存映射缓冲区
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    //------------------------------ 文件头 -------------------------------
    private final int hashSlotNum;// 哈希槽总数量
    private final int hashSlotNum_Pos = 0;

    private void writeSlotNum() {
        fileMap.putInt(hashSlotNum_Pos, this.hashSlotNum);
    }

    private int readSlotNum() {
        return fileMap.getInt(hashSlotNum_Pos);
    }

    private int hashSlotCount;// 索引文件中已使用哈希槽数量
    private final int hashSlotCount_Pos = 4;

    private void incHashSlotCount() {
        this.hashSlotCount++;
        writeHashSlotCount(this.hashSlotCount);
    }

    private void writeHashSlotCount(int count) {
        this.hashSlotCount = count;
        fileMap.putInt(hashSlotCount_Pos, count);
    }

    private int readHashSlotCount() {
        return fileMap.getInt(hashSlotCount_Pos);
    }

    /**
     * 索引文件已使用的索引条数
     * 注意：这里必须为1, 原因很简单, 我将0作为非法索引值, 因此0号索引被废弃
     * 哈希槽默认初始值为0, 因此0必须是非法索引值
     * 有效索引数量为indexCount-1
     */
    private int indexCount;
    private final int indexCount_Pos = 4 + 4;

    private void incIndexCount() {
        this.indexCount++;
        writeIndexCount(this.indexCount);
    }

    private void writeIndexCount(int count) {
        this.indexCount = count;
        fileMap.putInt(indexCount_Pos, count);
    }

    private int readIndexCount() {
        return fileMap.getInt(indexCount_Pos);
    }

    private static final int Index_Header_Size = 4 + 4 + 4;// 文件头长度
    //------------------------------ 文件头 -------------------------------


    public FileIndex(DataConf dataConf) throws IOException {
        this.maxIndexNum = dataConf.getMaxIndexNum();
        this.indexFileName = dataConf.getIndexFileName();
        // 初始化索引文件
        Path path = Path.of(dataConf.getDataDir(), this.indexFileName);
        boolean exists = Files.exists(path);// 是否存在
        MyFileUtil.ensureDirsAndFile(path);// 确保创建
        fileChannel = FileChannel.open(path, Set.of(StandardOpenOption.WRITE, StandardOpenOption.READ));
        // 文件映射: 映射整个文件
        // 存在则校验文件头
        if (exists) {
            fileMap = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0L, fileChannel.size());
            // 读取文件头并校验
            hashSlotNum = readSlotNum();
            hashSlotCount = readHashSlotCount();
            indexCount = readIndexCount();
            if (hashSlotNum != dataConf.getHashSlotNum()) {
                throw new RuntimeException(String.format("已存在的索引文件%s检测到文件头中的slotNum为%d，配置文件却给定为%d，两者必须相等", indexFileName, hashSlotNum, dataConf.getHashSlotNum()));
            }
            if (hashSlotCount < 0 || hashSlotCount >= hashSlotNum) {
                throw new RuntimeException(String.format("已存在的索引文件 %s 检测的 hashSlotCount: %d 不合法", indexFileName, hashSlotCount));
            }
            if (indexCount <= InvalidIndex || indexCount >= maxIndexNum) {
                throw new RuntimeException(String.format("已存在的索引文件 %s 检测的 indexCount: %d 不合法", indexFileName, indexCount));
            }
            long fileSize = Index_Header_Size + (long) Hash_Slot_Size * dataConf.getHashSlotNum() + (long) Index_Size * maxIndexNum;
            // 再校验文件大小
            if (fileSize != fileChannel.size()) {
                throw new RuntimeException(String.format("已存在的索引文件 %s 检测失败, 从配置计算理论size: %d, 但实际size: %d", indexFileName, fileSize, fileChannel.size()));
            }
        } else {
            fileMap = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0L, Index_Header_Size + (long) Hash_Slot_Size * dataConf.getHashSlotNum() + (long) Index_Size * maxIndexNum);
            // 不存在则初始化文件头
            hashSlotNum = dataConf.getHashSlotNum();
            writeSlotNum();
            hashSlotCount = 0;
            writeHashSlotCount(0);
            indexCount = 1;// 注意：这里必须为1, 原因很简单, 我将0作为非法索引值, 因此0号索引被废弃
            writeIndexCount(indexCount);
        }
        Logger.info(String.format("索引文件 %s 初始化完成, slotNum: %d, maxIndexNum: %d, hashSlotCount: %d, indexCount: %d", indexFileName, hashSlotNum, maxIndexNum, hashSlotCount, indexCount));
    }

    /**
     * 将meta.key插入索引文件
     * todo key唯一性校验
     *
     * @param meta   元信息
     * @param offset 元信息存储偏移量
     */
    public void putIndex(FileMeta meta, long offset, FileMetaStore metaStore) {
        if (indexCount >= maxIndexNum) {
            throw new RuntimeException(String.format("当前indexCount: %d 超过了maxIndexNum: %d, 无法继续插入哈希索引文件: %s", indexCount, maxIndexNum, indexFileName));
        }
        // 1.计算hash和哈希槽
        int hash = MyHash.hash(meta.getKey());
        int slotIdx = hash % hashSlotNum;
        int absoluteSlotPos = calculateHashSlotAbsPos(slotIdx);// 哈希槽的绝对位置

        String key = meta.getKey();
        lock.writeLock().lock();
        try {
            // 2.哈希槽旧值
            int oldSlotVal = fileMap.getInt(absoluteSlotPos);
            // 如果小于等于0或大于最大索引数，则说明此哈希槽没有被使用
            if (oldSlotVal <= InvalidIndex || oldSlotVal >= indexCount) {
                oldSlotVal = InvalidIndex;
            }

            Index oldIndex = null; // 该key可能存在的旧索引数据
            int oldIndexAbsPos = 0;
            Index deletedIndex = null;// 是否使用已经删除的索引数据空间可以使用
            int deletedIndexAbsPos = 0;
            // 3.链表查找
            // 3.1 先查找该key是否已经存在
            // 3.2 顺便找有没有已经删除的索引数据，如果有且key不存在则直接使用该索引空间，避免使用新空间
            int curIdx = oldSlotVal;// 当前索引的下标
            while (curIdx > InvalidIndex && curIdx < indexCount) {
                int curAbsPos = calculateIndexAbsPos(curIdx);// 当前索引存储的绝对位置
                Index cur = doReadIndex(curAbsPos);// 当前索引数据
                // 查找未删除的并比较hash
                if (cur.deleted == 0 && cur.hash == hash) {
                    // 再比较key
                    FileMeta oldMeta = metaStore.readMeta(cur.offset).meta;
                    if (key.equals(oldMeta.getKey())) {
                        // 该key已经存在
                        oldIndex = cur;
                        oldIndexAbsPos = curAbsPos;
                        break;
                    }
                } else if (cur.deleted == 1 && deletedIndex == null) {// 顺便找已经删除的索引数据
                    deletedIndex = cur;
                    deletedIndexAbsPos = curAbsPos;
                }
                // 发生冲突, 存在下一个索引则继续循环
                curIdx = cur.next;
            }
            // 3.1 该key已经存在，更新即可
            if (oldIndex != null) {
                Logger.info(String.format("文件key: %s, 即将插入索引文件，发现在索引文件中存在旧索引数据: %s, 将直接更新其offset为%d", key, oldIndex, offset));
                oldIndex.offset = offset;
                doWriteIndex(oldIndex, oldIndexAbsPos);
                return;
            } else if (deletedIndex != null) {
                // 3.2 存在删除的空索引数据可以使用
                Logger.debug(String.format("文件key: %s 插入索引文件，发现有删除的空索引数据供使用: %s，新offset为%d", key, deletedIndex, offset));
                deletedIndex.deleted = 0;
                deletedIndex.offset = offset;
                doWriteIndex(deletedIndex, deletedIndexAbsPos);
                return;
            }

            // 4.使用新索引空间并写入index
            int absoluteIndexPos = calculateIndexAbsPos(indexCount);
            Index index = new Index(hash, offset, 0, oldSlotVal);
            doWriteIndex(index, absoluteIndexPos);

            // 5.更新哈希槽
            fileMap.putInt(absoluteSlotPos, indexCount);// 注意这里存的不是absoluteIndexPos而是indexCount

            // 6.更新文件头
            // 6.1 使用了新的哈希槽
            if (oldSlotVal == InvalidIndex) {
                incHashSlotCount();
            }
            // 6.2 更新索引使用条数
            incIndexCount();
            Logger.info(String.format("文件 key: %s 已经插入索引文件, index: %s, 插入slotIdx: %d, 插入后 hashSlotCount: %d, indexCount: %d", meta.getKey(), index, slotIdx, hashSlotCount, indexCount));
        } finally {
            lock.writeLock().unlock();
        }
    }

    private void doWriteIndex(Index index, int absoluteIndexPos) {
        MappedByteBuffer slice = fileMap.slice();
        slice.position(absoluteIndexPos);
        slice.put(index.serialize());
    }

    /**
     * 逻辑删除
     *
     * @param key       唯一键
     * @param metaStore 元信息储存服务
     * @return 不存在返回null
     */
    public IndexWrapper deleteIndex(String key, FileMetaStore metaStore) {
        lock.writeLock().lock();
        try {
            IndexWrapper wrapper = getIndex(key, metaStore);
            if (wrapper != null) {
                // 删除并写入文件
                wrapper.index.deleted = 1;
                doWriteIndex(wrapper.index, wrapper.absoluteIndexPos);
            }
            return wrapper;
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 以key查offset
     *
     * @param key       文件唯一键
     * @param metaStore 元信息存储文件
     * @return 文件元信息存储偏移量, 没找到为null
     */
    public IndexWrapper getIndex(String key, FileMetaStore metaStore) {
        // 1.计算hash和哈希槽
        int hash = MyHash.hash(key);
        int slotIdx = hash % hashSlotNum;
        int absoluteSlotPos = Index_Header_Size + slotIdx * Hash_Slot_Size;// 哈希槽的绝对位置

        lock.readLock().lock();
        try {
            // 2.哈希槽值
            int slotVal = fileMap.getInt(absoluteSlotPos);
            // 哈希槽未使用
            if (slotVal <= InvalidIndex || slotVal >= indexCount) {
                return null;// 没找到
            }

            // 3.链表查找
            int curIdx = slotVal;// 当前索引的下标
            while (curIdx > InvalidIndex && curIdx < indexCount) {
                int curAbsPos = Index_Header_Size + Hash_Slot_Size * hashSlotNum + Index_Size * curIdx;// 当前索引存储的绝对位置
                Index curIndex = doReadIndex(curAbsPos);// 当前索引数据
                // 查找未删除的并比较hash
                if (curIndex.deleted == 0 && curIndex.hash == hash) {
                    // 再比较key
                    FileMeta meta = metaStore.readMeta(curIndex.offset).meta;
                    if (key.equals(meta.getKey())) {
                        // 找到了直接返回
                        return new IndexWrapper(curIdx, curAbsPos, curIndex);
                    }
                }
                // 发生冲突, 存在下一个索引则继续循环
                curIdx = curIndex.next;
            }
            return null;// 没找到
        } finally {
            lock.readLock().unlock();
        }
    }

    private Index doReadIndex(int absoluteIndexPos) {
        byte[] dest = new byte[Index_Size];
        fileMap.get(absoluteIndexPos, dest);
        ByteBuffer buf = ByteBuffer.wrap(dest);
        return Index.deserialize(buf);
    }

    // 计算哈希槽存储在文件的绝对位置
    private int calculateHashSlotAbsPos(int slotIdx) {
        return Index_Header_Size + // 文件头区域
                Hash_Slot_Size * slotIdx; // 哈希槽区域
    }

    // 计算索引数据存储在文件的绝对位置
    private int calculateIndexAbsPos(int indexIdx) {
        return Index_Header_Size + // 文件头区域
                Hash_Slot_Size * hashSlotNum + // 哈希槽区域
                Index_Size * indexIdx;// 索引存储的绝对位置
    }

    @Override
    public void close() throws IOException {
        fileMap.force();
        fileChannel.close();
    }

    // 索引条目
    public static class Index {
        public int hash;
        public long offset;
        public int deleted;// 0未删除，1已删除
        public int next;

        public Index(int hash, long offset, int deleted, int next) {
            this.hash = hash;
            this.offset = offset;
            this.deleted = deleted;
            this.next = next;
        }

        public ByteBuffer serialize() {
            ByteBuffer buffer = ByteBuffer.allocate(Index_Size);
            buffer.putInt(hash);
            buffer.putLong(offset);
            buffer.putInt(deleted);
            buffer.putInt(next);// 将哈希槽旧值写入索引条目的next，以形成链表解决哈希冲突
            buffer.flip();// 翻转待读
            return buffer;
        }

        public static Index deserialize(ByteBuffer buffer) {
            if (buffer.remaining() != Index_Size) {
                throw new RuntimeException(String.format("非法反序列化，需要字节数为%d，提供的buf字节数为%d", Index_Size, buffer.remaining()));
            }
            int hash = buffer.getInt();
            long offset = buffer.getLong();
            int deleted = buffer.getInt();
            int next = buffer.getInt();
            return new Index(hash, offset, deleted, next);
        }

        @Override
        public String toString() {
            return "Index{" +
                    "hash=" + hash +
                    ", offset=" + offset +
                    ", deleted=" + deleted +
                    ", next=" + next +
                    '}';
        }
    }

    @Data
    @AllArgsConstructor
    public static class IndexWrapper {
        public int indexIdx;// 当前索引的下标
        public int absoluteIndexPos;// 索引存储在文件的绝对位置
        public Index index;// 索引数据
    }
}
